53. 分布式搜索引擎

ES 集群

elasticsearch设计的理念就是分布式搜索引擎,底层其实还是基于lucene的。核心思想就是在多台机器上启动多个es进程实例,组成了一个es集群。

es 中存储数据的基本单位是索引,比如说你现在要在 es 中存储一些订单数据,你就应该在 es 中创建一个索引,order_idx,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。index -> type -> mapping -> document -> field。

index:mysql 里的一张表

type:没法跟 mysql 里去对比,一个index里可以有多个 type,每个 type 的字段都是差不多的,但是有一些略微的差别。

很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况,你可以认为index是一个类别的表,具体的每个 type 代表了具体的一个mysql中的表,每个 type 有一个 mapping,如果你认为一个type是一个具体的一个表,index 代表了多个 type 的同属于的一个类型,mapping 就是这个 type 的表结构定义。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了mysql 中某个表里的一行给,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。接着你搞一个索引,这个索引可以拆分成多个shard,每个shard存储部分数据。这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 primary shard,负责写入数据,但是还有几个 replica shard。primary shard 写入数据之后,会将数据同步到其他几个 replica shard 上去。通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,那也没关系。要是master节点宕机了,那么会重新选举一个节点为master 节点。如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。急着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。

ES 写数据过程

1)客户端选择一个 node 发送请求过去,这个 node 就是 coordinating node(协调节点)

2)coordinating node,对 document 进行路由,将请求转发给对应的 node(primary shard)

3)实际的 node 上的 primary shard 处理请求,然后将数据同步到 replica node

4)coordinating node,如果发现 primary node 和所有 replica node 都搞定之后,就返回响应结果给客户端

ES 读数据过程

1)客户端发送请求到任意一个 node,成为 coordinate node

2)coordinate node 对 document 进行路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡

3)接收请求的 node 返回 document 给 coordinate node

4)coordinate node 返回 document 给客户端

ES 搜索数据过程

1)客户端发送请求到一个 coordinate node

2)协调节点将搜索请求转发到所有的 shard 对应的 primary shard 或 replica shard 也可以

3)query phase:每个 shard 将自己的搜索结果(其实就是一些 doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果

4)fetch phase:接着由协调节点,根据doc id去各个节点上拉取实际的 document 数据,最终返回给客户端

写数据底层原理

1)先写入 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件

2)如果 buffer 快满了,或者到一定时间,就会将 buffer 数据 refresh 到一个新的 segment file 中,但是此时数据不是直接进入 segment file 的磁盘文件的,而是先进入 os cache 的。这个过程就是 refresh。

3)只要数据进入 os cache,此时就可以让这个 segment file 的数据对外提供搜索了

4)重复1~3步骤,新的数据不断进入 buffer 和 translog,不断将 buffer 数据写入一个又一个新的 segment file中去,每次 refresh 完 buffer 清空,translog 保留。随着这个过程推进,translo g会变得越来越大。当 translog达到一定长度的时候,就会触发 commit 操作。

5)当 translog 日志文件大到一定程度的时候,就会执行 commit 操作。commit 操作发生第一步,就是将 buffer中现有数据 refresh 到 os cache 中去,清空 buffer。

6)将一个 commit point 写入磁盘文件,里面标识着这个 commit point 对应的所有 segment file

7)强行将 os cache 中目前所有的数据都 fsync 到磁盘文件中去

8)将现有的 translog 清空,然后再次重启启用一个 translog,此时 commit 操作完成。默认每隔 30 分钟会自动执行一次 commit,但是如果 translog 过大,也会触发 commit。整个 commit 的过程,叫做 flush 操作。我们可以手动执行 flush 操作,就是将所有 os cache 数据刷到磁盘文件中去。

9)translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有5秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢5秒的数据。也可以将 translog 设置成每次写操作必须是直接 fsync 到磁盘,但是性能会差很多。

10)如果是删除操作,commit 的时候会生成一个.del 文件,里面将某个 doc 标识为 deleted 状态,那么搜索的时候根据 .del 文件就知道这个 doc 被删除了

11)如果是更新操作,就是将原来的 doc 标识为 deleted 状态,然后新写入一条数据

12)buffer 每次 refresh 一次,就会产生一个 segment file,所以默认情况下是1秒钟一个segment file,segment file 会越来越多,此时会定期执行merge (所以ES 是准实时的,因为数据 1 秒后写入 segment file 中后才会查询得到)

13)每次 merge 的时候,会将多个 segment file 合并成一个,同时这里会将标识为 deleted 的 doc 给物理删除掉,然后将新的 segment file 写入磁盘,这里会写一个 commit point,标识所有新的 segment file,然后打开segment file 供搜索使用,同时删除旧的 segment file。

translog日志文件的作用是什么?

就是在你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件,translog 日志文件中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。

性能优化

性能优化的杀手锏——filesystem cache

往 es 里写的数据,实际上都写到磁盘文件里去了,磁盘文件里的数据操作系统会自动将里面的数据缓存到 os cache 里面去,es 的搜索引擎严重依赖于底层的 filesystem cache,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 indx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。比如 ES 集群共有 360G 内存,那么就最好放不超过 720G 的数据,这样能保证有一半在缓存中。假如一共要在 es 中存储 1T 的数据,那么你的多台机器留个 filesystem cache 的内存加起来综合,至少要到 512G,至少半数的情况下,搜索是走内存的,性能一般可以到几秒钟。

比如说你现在有一行数据 id name age ….30个字段,但是你现在搜索,只需要根据 id name age 三个字段来搜索,如果你往 es 里写入一行数据所有的字段,就会导致说 70% 的数据是不用来搜索的,结果硬是占据了 es 机器上的 filesystem cache 的空间,单挑数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少,仅仅只是写入 es 中要用来检索的少数几个字段就可以了,比如说,就写入 id name age三个字段就可以了,然后你可以把其他的字段数据存在 mysql 里面,我们一般是建议用 es + hbase 的这么一个架构。hbase 的特点是适用于海量数据的在线存储,就是对 hbase 可以写入海量数据,不要做复杂的搜索,就是做很简单的一些根据id或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 doc id,然后根据 doc id 到 hbase 里去查询每个 doc id 对应的完整的数据,给查出来,再返回给前端。

数据预热

假如说,哪怕是你就按照上述的方案去做了,es集群中每个机器写入的数据量还是超过了filesystem cache一倍,比如说你写入一台机器60g数据,结果filesystem cache就30g,还是有30g数据留在了磁盘上。

举个例子,就比如说,微博,你可以把一些大v,平时看的人很多的数据给提前你自己后台搞个系统,每隔一会儿,你自己的后台系统去搜索一下热数据,刷到filesystem cache里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

电商,你可以将平时查看最多的一些商品,比如说iphone 8,热数据提前后台搞个程序,每隔1分钟自己主动访问一次,刷到filesystem cache里去。

对于那些你觉得比较热的,经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据,每隔一段时间,你就提前访问一下,让数据进入filesystem cache里面去。这样期待下次别人访问的时候,一定性能会好一些。

冷热分离

es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少,频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。你最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

document 模型设计

es里面的复杂的关联查询,复杂的查询语法,尽量别用。尽量都是展开得宽表数据。

分页性能优化

es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。

翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长。所以用 es 做分页的时候,会发现越翻到后面,就越是慢。

解决方法:

  1. 不允许深度分页
  2. 使用 scroll api。scroll 的原理实际上是保留一个数据快照,然后在一定时间内,你如果不断的滑动往后翻页的时候,类似于你现在在浏览微博,不断往下刷新翻页。那么就用 scroll 不断通过游标获取下一页数据。因为scroll api 是只能一页一页往后翻的,是不能说,先进入第 10 页,然后去 120 页,回到 58 页,不能随意乱跳页。